
#pragma once

#include "Prerequisites.h"
#include <algorithm>
#include <stack>
#include <vector>
#include "OgreMath/OgreVector3.h"
#include "OgreMath/OgreMatrix4.h"
#include "Ray.h"
#include "Mesh.h"
#include "Material.h"
#include "BVH.h"
#include "SphereBV.h"

class Surface : public BVH<DefaultBV, Surface>
{
protected:
	DefaultBV mBoundingVolume;
public:
	virtual ~Surface() {}

	/// Retrieves the surface material
	virtual const Material* getMaterial() const = 0;

	/// Computes the parameters for a fragment program given an intersection.
	/// @pre fragData.position is already computed.
	virtual void computeFragmentProgramData(FragmentProgramData &fragData, const Ray::Intersection &intersection) const = 0;

	/// Retrieves the bounding volume of the Surface.
	__forceinline DefaultBV getBoundingVolume() { return mBoundingVolume; }
};

class TriangleSurface : public Surface
{
private:
	TransformedMesh *mMesh;
	unsigned int mTriangleIndex, mIndex1, mIndex2, mIndex3;

public:
	TriangleSurface(TransformedMesh *mesh, unsigned int triangleIndex)
		: mMesh(mesh), mTriangleIndex(triangleIndex), mIndex1(mesh->getMesh()->indexBuffer[triangleIndex*3]), mIndex2(mesh->getMesh()->indexBuffer[triangleIndex*3+1]), mIndex3(mesh->getMesh()->indexBuffer[triangleIndex*3+2])
		{ mType = PRIMITIVE; }
	~TriangleSurface() {}
	const Surface* rayIntersectClosest(Ray::Intersection &intersection, const Ray &ray) const override
	{
		if (ray.intersectTriangle(intersection,
			mMesh->vertexBufferVS[mIndex1].position,
			mMesh->triangleDataVS[mTriangleIndex]))
			return this;
		return nullptr;
	}
	const Surface* rayIntersectUpdate(Ray::Intersection &intersection, const Ray &ray) const override
	{
		if (ray.intersectTriangleUpdate(intersection,
			mMesh->vertexBufferVS[mIndex1].position,
			mMesh->triangleDataVS[mTriangleIndex]))
			return this;
		return nullptr;
	}

	void computeFragmentProgramData(FragmentProgramData &fragData, const Ray::Intersection &intersection) const override
	{
		fragData.normal = mMesh->vertexBufferVS[mIndex1].normal*intersection.userData[0].z
			+ mMesh->vertexBufferVS[mIndex2].normal*intersection.userData[0].x
			+ mMesh->vertexBufferVS[mIndex3].normal*intersection.userData[0].y;
		fragData.normal.normalise();
		//fragData.normal = mMesh->triangleDataVS[mTriangleIndex].normal;
		fragData.userData[0] = intersection.userData[0];
	}

	const Material* getMaterial() const override { return &mMesh->material;/*triangleMaterials[mTriangleIndex];*/ }

	void updateBoundingVolume()
	{
		mBoundingVolume = DefaultBV(mMesh->vertexBufferVS[mIndex1].position, mMesh->vertexBufferVS[mIndex2].position, mMesh->vertexBufferVS[mIndex3].position);
	}
};

class Sphere : public Surface
{
public:
	float radius, radiusSquared;
	Ogre::Vector3 position;
	Material material;

	Sphere(const Ogre::Vector3 &_position, float _radius, const Material &_material)
		: position(_position), radius(_radius), radiusSquared(_radius*radius), material(_material) { updateBoundingVolume(); }
	~Sphere() {}

	mutable Ogre::Vector3 positionViewSpace;
	inline void applyModelView(const Ogre::Matrix4 &modelViewMatrix) const
	{
		positionViewSpace = modelViewMatrix * position;
	}
	const Surface* rayIntersectClosest(Ray::Intersection &intersection, const Ray &ray) const override
	{
		if (ray.intersectSphere(intersection, positionViewSpace, radiusSquared)) return this;
		return nullptr;
	}

	void computeFragmentProgramData(FragmentProgramData &fragData, const Ray::Intersection &intersection) const override
	{
		fragData.normal = fragData.position - positionViewSpace;
		fragData.normal.normalise();
	}

	void updateBoundingVolume()
	{
		mBoundingVolume = DefaultBV(positionViewSpace, radius);
	}

	const Material* getMaterial() const override { return &material; }
};

/*class Plane
{
public:
	Plane(const Ogre::Vector3 &_normal,
		const Ogre::Vector3 &_position,
		const Material &_material) : normal(_normal), position(_position), material(_material) {}
	~Plane() {}
	Ogre::Vector3 normal;
	Ogre::Vector3 position;
	Material material;

	mutable Ogre::Vector3 normalViewSpace, positionViewSpace;
	inline void applyModelView(const Ogre::Matrix4 &modelViewMatrix) const
	{
		Ogre::Matrix3 rotMatrix;
		modelViewMatrix.extract3x3Matrix(rotMatrix);
		normalViewSpace = rotMatrix * normal;	//apply only the rotation the normal
		positionViewSpace = modelViewMatrix * position;		
	}
	const Material* getMaterial() const { return &material; }
};*/